package br.ufpe.cin.imlm2.jbeat.soot.dump;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import soot.ArrayType;
import soot.NullType;
import soot.PrimType;
import soot.RefType;
import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.Type;
import br.ufpe.cin.imlm2.jbeat.model.ILightWeightType;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightClass;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightMethod;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightScene;
import br.ufpe.cin.imlm2.jbeat.model.factory.ClassMaker;
import br.ufpe.cin.imlm2.jbeat.model.factory.MethodMaker;
import br.ufpe.cin.imlm2.jbeat.model.factory.TypeMaker;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.PropagatesToTag;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.ThrowableSetTag;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.TrapedSetTag;

public class ClassResolver {

	private TypeMaker tm;
	private ClassMaker cm;

	public ClassResolver() {
		this.tm = new TypeMaker();
		this.cm = new ClassMaker(this.tm);
	}

	public LightWeightScene resolveSootClasses(Collection<SootClass> classes) {
		
		for (SootClass sootClass : classes) {
			resolveClass(sootClass);
		}

		LightWeightScene scene = new LightWeightScene();
		for (ILightWeightType type : this.tm.retrieveTypes()) {
			scene.addType(type);
		}
		for (LightWeightClass c : this.cm.retrieveAllClasses()) {
			scene.addClass(c);
		}
		List<SootMethod> entryPoints = Scene.v().getEntryPoints();
		for (SootMethod meth : entryPoints) {
			scene.addEntryPoint(this.getMethod(meth));
		}
		scene.setMain(this.getMethod(Scene.v().getMainMethod()));
		
		return scene;
	}
	
	@SuppressWarnings("unchecked")
	private LightWeightMethod getMethod(SootMethod m){
		LightWeightClass c = this.cm.retrieveClass(m.getDeclaringClass().getName());
		MethodMaker mm = new MethodMaker(c);
		LightWeightMethod lightMeth = mm.retrieveMethod(m.getName(),
				this.makeTypes(m.getParameterTypes()));
		return lightMeth;
	}

	/*
	 * Is mutual recursion between the resolveClass and the resolveMethod methods a good and 
	 * safe solution?
	 */

	private LightWeightClass resolveClass(SootClass sootClass) {
		LightWeightClass c = this.cm.retrieveClass(sootClass.getName());
		MethodMaker mm = new MethodMaker(c);
		for (SootMethod meth : sootClass.getMethods()) {
			resolveMethod(mm, meth);
		}
		return c;
	}

	@SuppressWarnings("unchecked")
	private LightWeightMethod resolveMethod(MethodMaker mm, SootMethod meth) {
		LightWeightMethod lightMeth = mm.retrieveMethod(meth.getName(), 
				this.makeTypes(meth.getParameterTypes()));
		if(meth.hasTag(ThrowableSetTag.TAG_NAME)) {
			ThrowableSetTag tst = (ThrowableSetTag) meth.getTag(ThrowableSetTag.TAG_NAME); 
			lightMeth.getGeneratedSet().addAll(this.makeClasses(tst.getAllGeneratedExceptions()));
			lightMeth.getThrownSet().addAll(this.makeClasses(tst.getAllThrownExceptions()));
			lightMeth.getPropagatedMap().putAll(this.makeMap(tst.getPropagatedMap()));
			lightMeth.getRethrownMap().putAll(this.makeMap(tst.getRethrownMap()));
		}
		if(meth.hasTag(TrapedSetTag.TAG_NAME)){
			TrapedSetTag trapetSetTag = (TrapedSetTag) meth.getTag(TrapedSetTag.TAG_NAME);
			lightMeth.getTrappedMap().putAll(this.makeMap(trapetSetTag.getTrappedMap()));
		}
		if(meth.hasTag(PropagatesToTag.TAG_NAME)){
			PropagatesToTag ptt = (PropagatesToTag) meth.getTag(PropagatesToTag.TAG_NAME);
			lightMeth.getPropagatesToSet().addAll(this.makeSet(ptt.getMethods()));
		}
		return lightMeth;
	}

	@SuppressWarnings("unchecked")
	private Map<? extends LightWeightClass, ? extends List<LightWeightMethod>> makeMap(
			Map<SootClass, Set<SootMethod>> propagatedMap) {
		Map<LightWeightClass,List<LightWeightMethod>> lightMap = new HashMap<>();
		for (Entry<SootClass, Set<SootMethod>> e: propagatedMap.entrySet()) {
			LightWeightClass excep = this.cm.retrieveClass(e.getKey().getName());
			List<LightWeightMethod> sources = new ArrayList<>();
			for (SootMethod sm : e.getValue()) {
				LightWeightClass decClass = this.cm.retrieveClass(sm.
						getDeclaringClass().getName());
				MethodMaker mm = new MethodMaker(decClass);
				LightWeightMethod lightMeth = mm.retrieveMethod(sm.getName(), 
						this.makeTypes(sm.getParameterTypes()));
				sources.add(lightMeth);
			}
			lightMap.put(excep, sources);
		}
		return lightMap;
	}
	
	private Set<LightWeightMethod> makeSet(Set<SootMethod> set){
		Set<LightWeightMethod> outSet = new HashSet<>();
		for (SootMethod sootMethod : set) {
			outSet.add(this.getMethod(sootMethod));
		}
		return outSet;
	}

	private List<ILightWeightType> makeTypes(Collection<Type> types){
		List<ILightWeightType> newTypes = new ArrayList<>();
		for (Type type : types) {
			newTypes.add(this.resolveType(type));
		}
		return newTypes;
	}

	private List<LightWeightClass> makeClasses(Collection<SootClass> classes) {
		List<LightWeightClass> lightClasses = new ArrayList<>();
		for (SootClass sootClass : classes) {
			lightClasses.add(this.cm.retrieveClass(sootClass.getName()));
		}		
		return lightClasses;
	}

	private ILightWeightType resolveType(Type t){
		if(t instanceof PrimType)
			return this.tm.retrieveType(t.toString());
		else if(t instanceof RefType)
			return this.tm.retrieveReferenceType(t.toString(), null);
		else if(t instanceof NullType)
			return this.tm.retrieveReferenceType(t.toString(), null);
		else if(t instanceof ArrayType) {
			ArrayType arrType = (ArrayType) t;
			return this.tm.retrieveVectorType(this.resolveType(arrType.baseType),
					arrType.numDimensions);
		}
		return this.tm.retrieveType(t.toString());
	}
}
